// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using JetBrains.Annotations;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    public abstract class TypeMapper : ITypeMapper
    {
        protected readonly IDataContextOptions _contextOptions;

        public TypeMapper(IDataContextOptions contextOptions)
        {
            _contextOptions = contextOptions;
        }

        private readonly ConcurrentDictionary<string, TypeMapping> _explicitMappings
            = new ConcurrentDictionary<string, TypeMapping>();


        /// <summary>
        ///     Gets the mapper to be used for byte array properties.
        /// </summary>
        public virtual IByteArrayTypeMapper ByteArrayMapper => null;

        /// <summary>
        ///     Gets the mapper to be used for string properties.
        /// </summary>
        public virtual IStringTypeMapper StringMapper => null;

        /// <summary>
        ///     Gets a value indicating whether the given .NET type is mapped.
        /// </summary>
        /// <param name="clrType"> The .NET type. </param>
        /// <returns> True if the type can be mapped; otherwise false. </returns>
        public virtual bool IsTypeMapped(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            return this.FindMapping(clrType) != null;
        }

        /// <summary>
        ///     Ensures that the given type name is a valid type for the relational database.
        ///     An exception is thrown if it is not a valid type.
        /// </summary>
        /// <param name="dbTypeName">The type to be validated.</param>
        public virtual void ValidateTypeName(string dbTypeName)
        {
        }

        /// <summary>
        ///     Gets the relational database type for a given .NET type.
        ///     Returns null if no mapping is found.
        /// </summary>
        /// <param name="clrType">The type to get the mapping for.</param>
        /// <returns>
        ///     The type mapping to be used.
        /// </returns>
        public virtual TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            return this.GetClrTypeMappings().TryGetValue(TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType)), out var mapping)
                ? mapping
                : null;
        }

        /// <summary>
        ///     Gets the mapping that represents the given database type.
        ///     Returns null if no mapping is found.
        /// </summary>
        /// <param name="dbTypeName">The type to get the mapping for.</param>
        /// <returns>
        ///     The type mapping to be used.
        /// </returns>
        public virtual TypeMapping FindMapping(string dbTypeName)
        {
            Check.NotNull(dbTypeName, nameof(dbTypeName));

            return _explicitMappings.GetOrAdd(dbTypeName, this.CreateMappingFromDbType);
        }

        public virtual TypeMapping FindMapping(Type clrType, string dbTypeName)
        {
            Check.NotNull(clrType, nameof(clrType));

            return this.FindMapping(clrType);
        }

        public ValueConverter GetStringValueConverter(Type dbFileType)
        {
            return new DefaultValueConverter(typeof(string), dbFileType);
        }

        /// <summary>
        ///     Gets the mappings from .NET types to database types.
        /// </summary>
        /// <returns> The type mappings. </returns>
        protected abstract IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings();

        /// <summary>
        ///     Gets the mappings from database types to .NET types.
        /// </summary>
        /// <returns> The type mappings. </returns>
        protected abstract IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings();

        /// <summary>
        ///     Creates the mapping for the given database type.
        /// </summary>
        /// <param name="dbTypeName">The type to create the mapping for.</param>
        /// <returns> The type mapping to be used. </returns>
        protected virtual TypeMapping CreateMappingFromDbType([NotNull] string dbTypeName)
        {
            Check.NotNull(dbTypeName, nameof(dbTypeName));

            if (this.GetDbTypeMappings().TryGetValue(dbTypeName, out var mapping)
                && mapping.DbTypeName.Equals(dbTypeName, StringComparison.OrdinalIgnoreCase))
            {
                return mapping;
            }

            var openParen = dbTypeName.IndexOf("(", StringComparison.Ordinal);
            if (openParen > 0)
            {
                var closeParen = dbTypeName.IndexOf(")", openParen + 1, StringComparison.Ordinal);

                var typeName = dbTypeName.Substring(0, openParen);

                if (closeParen > 0)
                {
                    typeName = dbTypeName.Remove(openParen, closeParen - openParen + 1);
                }

                if (!this.GetDbTypeMappings().TryGetValue(typeName, out mapping))
                {
                    return null;
                }

                if (mapping.ClrType == typeof(string)
                    || mapping.ClrType == typeof(byte[]))
                {
                    if (closeParen > openParen
                        && Int32.TryParse(dbTypeName.Substring(openParen + 1, closeParen - openParen - 1), out var size)
                        && mapping.Size != size)
                    {
                        return mapping.Clone(dbTypeName, size);
                    }
                }
            }

            return mapping?.Clone(dbTypeName, mapping.Size);
        }
    }
}
